package br.com.afi.pattern.dao.hibernate.jpa;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.text.MessageFormat;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import br.com.afi.pattern.dao.GenericDAO;
import br.com.afi.pattern.model.Manageable;
import br.com.afi.pattern.model.Model;
import br.com.afi.util.hibernate.CriteriaUtil;

/**
 * Essa classe possui os métodos genéricos do JPA que utiliza a implementação do Hibernate para
 * todos os DAOs.
 * 
 * @author André de Fontana Ignacio
 * @param <E> Tipo da Entity
 * @param <ID> Tipo da chave primária do Model(Entity)
 */
public abstract class GenericJpaHibernateDAO<E extends Manageable & Model<ID>, ID extends Serializable> implements GenericDAO<E, ID>
{
	private Class<E> persistentClass;

	private String findAllQuery;

	/**
	 * Construtor
	 */
	@SuppressWarnings("unchecked")
	public GenericJpaHibernateDAO()
	{
		this.persistentClass = ( Class<E> ) ( ( ParameterizedType ) getClass().getGenericSuperclass() ).getActualTypeArguments()[0];
		this.findAllQuery = MessageFormat.format( "from {0}", persistentClass.getName() );
	}

	/**
	 * Construtor
	 * 
	 * @param findAllQuery Query utilizada no findAll
	 */
	@SuppressWarnings("unchecked")
	public GenericJpaHibernateDAO( String findAllQuery )
	{
		this.persistentClass = ( Class<E> ) ( ( ParameterizedType ) getClass().getGenericSuperclass() ).getActualTypeArguments()[0];
		this.findAllQuery = findAllQuery;
	}

	/**
	 * Atribui o EntityManager que será utilizado.
	 * 
	 * @return Gerenciador de Entidades
	 */
	protected abstract EntityManager getEntityManager();

	/**
	 * Atribui a queryme EJB-QL que deve ser utilizada para o findAll.
	 * 
	 * @param findAllQuery Query em EJB-QL
	 */
	protected void setFindAllQuery( String findAllQuery )
	{
		this.findAllQuery = findAllQuery;
	}

	/**
	 * Retorna a classe da Entity deste DAO.
	 * 
	 * @return Classe da Entity
	 */
	public Class<E> getPersistentClass()
	{
		return persistentClass;
	}

	/**
	 * Obtém a session do hibernate através do delegate.
	 * 
	 * @return Session
	 */
	public Session getSession()
	{
		return ( Session ) getEntityManager().getDelegate();
	}

	/**
	 * Obtém o primeiro objeto da lista, se não existir retorna nulo.
	 * 
	 * @param list Lista
	 * @return Primeiro objeto da lista, se não existir retorna nulo.
	 */
	public E uniqueResult( List<E> list )
	{
		return list.isEmpty() ? null : list.get( 0 );
	}

	/**
	 * Obtém o primeiro objeto da lista, se não existir retorna nulo.
	 * 
	 * @param list Lista
	 * @return Primeiro objeto da lista, se não existir retorna nulo.
	 */
	public Object uniqueResultObject( List<? extends Object> list )
	{
		return list.isEmpty() ? null : list.get( 0 );
	}

	@Override
	public Long countAll()
	{
		return ( Long ) getEntityManager().createQuery( MessageFormat.format( "Select count from {0}", persistentClass.getName() ) ).getSingleResult();
	}

	@Override
	public Number countByExample( E exampleInstance, String... excludeProperty )
	{
		final Criteria crit = getSession().createCriteria( getPersistentClass() );
		final Example example = Example.create( exampleInstance );
		for ( String exclude : excludeProperty )
		{
			example.excludeProperty( exclude );
		}
		crit.setResultTransformer( Criteria.DISTINCT_ROOT_ENTITY );
		crit.setProjection( Projections.rowCount() );
		crit.add( example );
		return ( Number ) crit.uniqueResult();
	}

	@Override
	public Number countByExampleILike( E exampleInstance, boolean includeRelationships, String... excludeProperty )
	{
		final Criteria crit = getSession().createCriteria( getPersistentClass() );
		final Example example = Example.create( exampleInstance );
		example.enableLike();
		example.ignoreCase();
		for ( String exclude : excludeProperty )
		{
			example.excludeProperty( exclude );
		}
		crit.setResultTransformer( Criteria.DISTINCT_ROOT_ENTITY );
		crit.setProjection( Projections.rowCount() );
		if ( includeRelationships )
		{
			final List<Criterion> criterios = CriteriaUtil.buildRelationshipsCriterions( exampleInstance );
			for ( Criterion criterion : criterios )
			{
				crit.add( criterion );
			}
		}
		crit.add( example );
		return ( Number ) crit.uniqueResult();
	}

	@Override
	public boolean existsById( ID id )
	{
		final Criteria crit = getSession().createCriteria( getPersistentClass() );
		crit.setResultTransformer( Criteria.DISTINCT_ROOT_ENTITY );
		crit.setProjection( Projections.rowCount() );
		crit.add( Restrictions.idEq( id ) );
		final Number count = ( Number ) crit.uniqueResult();
		return ! ( count.equals( 0 ) && count.equals( 0L ) );
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findAll()
	{
		return getEntityManager().createQuery( findAllQuery ).getResultList();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findAll( int firstResult, int maxResults )
	{
		final Query query = getEntityManager().createQuery( findAllQuery );
		query.setFirstResult( firstResult );
		query.setMaxResults( maxResults );
		return query.getResultList();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findByExample( E exampleInstance, String... excludeProperty )
	{
		final Criteria crit = getSession().createCriteria( getPersistentClass() );
		final Example example = Example.create( exampleInstance );
		for ( String exclude : excludeProperty )
		{
			example.excludeProperty( exclude );
		}
		crit.add( example );
		return crit.list();
	}

	@Override
	public List<E> findByExampleILike( E exampleInstance, String... excludeProperty )
	{
		return this.findByExampleILike( exampleInstance, false, excludeProperty );
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findByExampleILike( E exampleInstance, boolean includeRelationships, String... excludeProperty )
	{
		final Criteria crit = getSession().createCriteria( getPersistentClass() );
		final Example example = Example.create( exampleInstance );
		example.enableLike();
		example.ignoreCase();
		for ( String exclude : excludeProperty )
		{
			example.excludeProperty( exclude );
		}
		if ( includeRelationships )
		{
			final List<Criterion> criterios = CriteriaUtil.buildRelationshipsCriterions( exampleInstance, excludeProperty );
			for ( Criterion criterion : criterios )
			{
				crit.add( criterion );
			}
		}
		crit.add( example );
		return crit.list();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findByExampleILike( E exampleInstance, boolean includeRelationships, int firstResult, int maxResults, String... excludeProperty )
	{
		final Criteria crit = getSession().createCriteria( getPersistentClass() );
		final Example example = Example.create( exampleInstance );
		example.enableLike();
		example.ignoreCase();
		for ( String exclude : excludeProperty )
		{
			example.excludeProperty( exclude );
		}
		if ( includeRelationships )
		{
			final List<Criterion> criterios = CriteriaUtil.buildRelationshipsCriterions( exampleInstance );
			for ( Criterion criterion : criterios )
			{
				crit.add( criterion );
			}
		}
		crit.add( example );
		crit.setFirstResult( firstResult );
		crit.setMaxResults( maxResults );
		return crit.list();
	}

	@Override
	/*
	 * <b>Atenção lock não implementado</b>
	 */
	public E findById( ID id, boolean lock )
	{
		return getEntityManager().find( getPersistentClass(), id );
	}

	@Override
	public E findById( ID id )
	{
		return getEntityManager().find( getPersistentClass(), id );
	}

	@Override
	public E makePersistent( E entity )
	{
		if ( entity.isManaged() )
		{
			entity = getEntityManager().merge( entity );
		}
		else
		{
			getEntityManager().persist( entity );
		}
		return entity;
	}

	@Override
	public void makeTransient( E entity )
	{
		entity = getEntityManager().merge( entity );
		getEntityManager().remove( entity );
	}

	/**
	 * Efetiva o envio das informações.
	 */
	public void flush()
	{
		getEntityManager().flush();
	}

	/**
	 * Limpa o cache.
	 */
	public void clear()
	{
		getEntityManager().clear();
	}
}
